// SPDX-FileCopyrightText: Adam Evyčędo
//
// SPDX-License-Identifier: AGPL-3.0-or-later

package traffic

import (
	pb "apiote.xyz/p/szczanieckiej/gtfs_rt/transit_realtime"

	"errors"
	"fmt"
	"log"
	"os"
	"strings"
	"sync"
	"time"

	"git.sr.ht/~sircmpwn/go-bare"
	"golang.org/x/text/language"
)

type BlockingError struct {
	cause error
}

func (e BlockingError) Error() string {
	return e.cause.Error()
}

type Alerts struct {
	ByLine     map[string][]uint
	ByTrip     map[string][]uint
	ByLineType map[LineType][]uint
	ByStop     map[string][]uint
	ByAgency   map[string][]uint
	Alerts     []Alert
}

type SpecificAlert struct {
	Header      string
	Description string
	URL         string
	Cause       AlertCause
	Effect      AlertEffect
}

type Alert struct {
	TimeRanges   [][2]time.Time
	Headers      map[language.Tag]string
	Descriptions map[language.Tag]string
	URLs         map[language.Tag]string
	Cause        AlertCause
	Effect       AlertEffect
}

type AlertCause uint

const (
	CAUSE_UNKNOWN           AlertCause = 0
	CAUSE_OTHER             AlertCause = 1
	CAUSE_TECHNICAL_PROBLEM AlertCause = 2
	CAUSE_STRIKE            AlertCause = 3
	CAUSE_DEMONSTRATION     AlertCause = 4
	CAUSE_ACCIDENT          AlertCause = 5
	CAUSE_HOLIDAY           AlertCause = 6
	CAUSE_WEATHER           AlertCause = 7
	CAUSE_MAINTENANCE       AlertCause = 8
	CAUSE_CONSTRUCTION      AlertCause = 9
	CAUSE_POLICE_ACTIVITY   AlertCause = 10
	CAUSE_MEDICAL_EMERGENCY AlertCause = 11
)

func alertCauseOfGtfs(v *pb.Alert_Cause) AlertCause {
	switch v {
	case pb.Alert_UNKNOWN_CAUSE.Enum():
		return CAUSE_UNKNOWN
	case pb.Alert_OTHER_CAUSE.Enum():
		return CAUSE_OTHER
	case pb.Alert_TECHNICAL_PROBLEM.Enum():
		return CAUSE_TECHNICAL_PROBLEM
	case pb.Alert_STRIKE.Enum():
		return CAUSE_STRIKE
	case pb.Alert_DEMONSTRATION.Enum():
		return CAUSE_DEMONSTRATION
	case pb.Alert_ACCIDENT.Enum():
		return CAUSE_ACCIDENT
	case pb.Alert_HOLIDAY.Enum():
		return CAUSE_HOLIDAY
	case pb.Alert_WEATHER.Enum():
		return CAUSE_WEATHER
	case pb.Alert_MAINTENANCE.Enum():
		return CAUSE_MAINTENANCE
	case pb.Alert_CONSTRUCTION.Enum():
		return CAUSE_CONSTRUCTION
	case pb.Alert_POLICE_ACTIVITY.Enum():
		return CAUSE_POLICE_ACTIVITY
	case pb.Alert_MEDICAL_EMERGENCY.Enum():
		return CAUSE_MEDICAL_EMERGENCY
	default:
		return CAUSE_UNKNOWN
	}
}

type AlertEffect uint

const (
	EFFECT_UNKNOWN             AlertEffect = 0
	EFFECT_OTHER               AlertEffect = 1
	EFFECT_NO_SERVICE          AlertEffect = 2
	EFFECT_REDUCED_SERVICE     AlertEffect = 3
	EFFECT_SIGNIFICANT_DELAYS  AlertEffect = 4
	EFFECT_DETOUR              AlertEffect = 5
	EFFECT_ADDITIONAL_SERVICE  AlertEffect = 6
	EFFECT_MODIFIED_SERVICE    AlertEffect = 7
	EFFECT_STOP_MOVED          AlertEffect = 8
	EFFECT_NONE                AlertEffect = 9
	EFFECT_ACCESSIBILITY_ISSUE AlertEffect = 10
)

func alertEffectOfGtfs(v *pb.Alert_Effect) AlertEffect {
	switch v {
	case pb.Alert_UNKNOWN_EFFECT.Enum():
		return EFFECT_UNKNOWN
	case pb.Alert_OTHER_EFFECT.Enum():
		return EFFECT_OTHER
	case pb.Alert_NO_SERVICE.Enum():
		return EFFECT_NO_SERVICE
	case pb.Alert_REDUCED_SERVICE.Enum():
		return EFFECT_REDUCED_SERVICE
	case pb.Alert_SIGNIFICANT_DELAYS.Enum():
		return EFFECT_SIGNIFICANT_DELAYS
	case pb.Alert_DETOUR.Enum():
		return EFFECT_DETOUR
	case pb.Alert_ADDITIONAL_SERVICE.Enum():
		return EFFECT_ADDITIONAL_SERVICE
	case pb.Alert_MODIFIED_SERVICE.Enum():
		return EFFECT_MODIFIED_SERVICE
	case pb.Alert_STOP_MOVED.Enum():
		return EFFECT_STOP_MOVED
	case pb.Alert_NO_EFFECT.Enum():
		return EFFECT_NONE
	case pb.Alert_ACCESSIBILITY_ISSUE.Enum():
		return EFFECT_ACCESSIBILITY_ISSUE
	default:
		return EFFECT_UNKNOWN
	}
}

// ............ feedID     trip/stop
var updates map[string]map[string][]Update
var alerts map[string]Alerts
var vehicleStatuses map[string]map[string]VehicleStatus
var cacheMx sync.Mutex

func getTripID(tripsFile *os.File, offset int64) (string, error) {
	_, err := tripsFile.Seek(offset, 0)
	if err != nil {
		return "", fmt.Errorf("while seeking: %w", err)
	}
	trip := Trip{}
	err = bare.UnmarshalReader(tripsFile, &trip)
	if err != nil {
		return "", fmt.Errorf("while unmarshalling: %w", err)
	}
	return trip.Id, nil
}

func departuresFromNoTripUpdates(updates []Update, alerts map[string][]Alert, pickups, dropoffs map[string]Boarding, timezone *time.Location, languages []language.Tag) ([]DepartureRealtime, error) {
	departures := []DepartureRealtime{}
	now := time.Now().In(timezone)
	for _, update := range updates {
		if update.Time == "" {
			log.Printf("update time is empty, update is %+v\n", update)
			continue
		}
		departureTime, err := time.Parse("150405", update.Time)
		if err != nil {
			return departures, fmt.Errorf("while parsing time: %w", err)
		}
		departureTime = time.Date(now.Year(), now.Month(), now.Day(), departureTime.Hour(), departureTime.Minute(), departureTime.Second(), 0, timezone)
		departures = append(departures, DepartureRealtime{
			Time: departureTime,
			Departure: Departure{
				Pickup:  pickups[update.VehicleStatus.LineID],
				Dropoff: dropoffs[update.VehicleStatus.LineID],
			},
			Headsign: update.VehicleStatus.Headsign,
			LineID:   update.VehicleStatus.LineID,
			Order: StopOrder{
				uint(departureTime.Unix()),
				0,
			},
			Update: update, // NOTE delay must be 0
			Alerts: selectSpecificAlerts(alerts[update.VehicleStatus.TripID], languages),
		})
	}

	return departures, nil
}

func enrichDepartures(stopID, stopCode string, departures []DepartureRealtime, datetime time.Time, departuresType DeparturesType, ctx Context, tripsFile *os.File, timezone *time.Location, languages []language.Tag) ([]DepartureRealtime, error) { // TODO tripsFile -> map[tripOffset]tripID
	enrichedDepartures := make([]DepartureRealtime, len(departures))

	feedInfo, err := getFeedInfo(ctx.DataHome, ctx.FeedID, ctx.Version)
	if err != nil {
		log.Printf("while getting feedInfo: %v\n", err)
		feedInfo = FeedInfo{}
	}

	var enrichMethod func(string, int, string, string, Context) (map[string][]Update, map[string][]Alert, bool, error)
	if feedInfo.Name != "" {
		if _, ok := feedInfo.RealtimeFeeds[TRIP_UPDATES]; ok {
			enrichMethod = getGtfsRealtimeUpdates
			// log.Println("GTFS")
		} else if isLuaUpdatesScript(ctx) {
			enrichMethod = getLuaRealtimeUpdates
			// log.Println("Lua")
		} else {
			// log.Println("none")
		}
	}

	offsets := make([]uint, len(departures))
	pickups := map[string]Boarding{}
	dropoffs := map[string]Boarding{}
	for i, departure := range departures {
		offsets[i] = departure.Order.TripOffset
		pickups[departure.LineID] = departure.Departure.Pickup
		dropoffs[departure.LineID] = departure.Departure.Dropoff
	}
	trips, err := GetTripsByOffset(offsets, ctx, func(Trip) bool { return true })
	if err != nil {
		return departures, fmt.Errorf("while getting trips: %w", err)
	}

	midnight := time.Date(datetime.Year(), datetime.Month(),
		datetime.Day(), 0, 0, 0, 0, timezone)
	if departuresType == DEPARTURES_HYBRID {
		for i, departure := range departures {
			if departure.Time.After(midnight) {
				var (
					updates             map[string][]Update
					alerts              map[string][]Alert
					areTripsInTimetable bool
				)
				if enrichMethod != nil {
					updates, alerts, areTripsInTimetable, err = enrichMethod(trips[departure.Order.TripOffset].Id, departure.Order.Sequence, stopID, stopCode, ctx)
					if err != nil {
						var ber BlockingError
						if isTimeout(err) || errors.As(err, &ber) || strings.Contains(err.Error(), "connection refused") { // TODO or any other connection problem
							log.Printf("blocking error while enriching departure %s -> %s (%v): %v", departure.LineID, departure.Headsign, departure.Time, err)
							update := Update{}
							update.VehicleStatus.LineID = trips[departure.Order.TripOffset].LineID
							update.VehicleStatus.Headsign = trips[departure.Order.TripOffset].Headsign
							enrichedDepartures[i] = departure.WithUpdate(update)
							enrichMethod = nil
							continue
						} else {
							log.Printf("while enriching departure %s -> %s (%v): %v\n", departure.LineID, departure.Headsign, departure.Time, err)
							enrichedDepartures[i] = departure
							continue
						}
					}
					if areTripsInTimetable {
						tripUpdates := updates[trips[departure.Order.TripOffset].Id]
						var validTripUpdate Update
						for _, tripUpdate := range tripUpdates {
							if tripUpdate.StopSequence > uint32(departure.Order.Sequence) {
								break
							}
							validTripUpdate.Time = tripUpdate.Time
							validTripUpdate.Delay = tripUpdate.Delay
							validTripUpdate.StopID = tripUpdate.StopID
							validTripUpdate.StopSequence = tripUpdate.StopSequence
							validTripUpdate.Time = tripUpdate.Time
							validTripUpdate.TimetableRelationship = tripUpdate.TimetableRelationship
							validTripUpdate.VehicleStatus = tripUpdate.VehicleStatus
						}
						validTripUpdate.VehicleStatus.LineID = trips[departure.Order.TripOffset].LineID
						validTripUpdate.VehicleStatus.Headsign = trips[departure.Order.TripOffset].Headsign
						enrichedDepartures[i] = departure.WithUpdate(validTripUpdate)
						enrichedDepartures[i] = enrichedDepartures[i].WithAlerts(alerts[trips[departure.Order.TripOffset].Id], languages)
					} else {
						var err error
						enrichedDepartures, err = departuresFromNoTripUpdates(updates[stopCode], alerts, pickups, dropoffs, timezone, languages)
						if err != nil {
							return departures, fmt.Errorf("while creating departures without trip: %w", err)
						}
						break
					}
				} else {
					update := Update{}
					update.VehicleStatus.LineID = trips[departure.Order.TripOffset].LineID
					update.VehicleStatus.Headsign = trips[departure.Order.TripOffset].Headsign
					enrichedDepartures[i] = departure.WithUpdate(update)
				}
			}
		}
	} else {
		for i, departure := range departures {
			enrichedDepartures[i] = departure.WithUpdate(Update{
				VehicleStatus: VehicleStatus{
					LineID:   trips[departure.Order.TripOffset].LineID,
					Headsign: trips[departure.Order.TripOffset].Headsign,
				},
			})
		}
	}
	return enrichedDepartures, nil
}

func GetAlerts(stopID, stopCode string, tripOffset int, ctx Context, t *Traffic, languages []language.Tag) []SpecificAlert {
	feedInfo, err := getFeedInfo(ctx.DataHome, ctx.FeedID, ctx.Version)
	if err != nil {
		log.Printf("while getting feedInfo: %v\n", err)
		feedInfo = FeedInfo{}
	}

	var function func(string, string, string, Context, *Traffic) ([]Alert, error)
	if feedInfo.Name != "" {
		if _, ok := feedInfo.RealtimeFeeds[ALERTS]; ok {
			function = getGtfsRealtimeAlerts
		} else if isLuaAlertsScript(ctx) {
			function = getLuaRealtimeAlerts
		} else {
			return []SpecificAlert{}
		}
	}

	tripID := ""
	if tripOffset > 0 {
		trip, err := GetTripByOffset(uint(tripOffset), ctx, t)
		if err != nil {
			log.Printf("while getting trip: %v\n", err)
			return []SpecificAlert{}
		}
		tripID = trip.Id
	}

	if function != nil {
		alerts, err := function(stopID, stopCode, tripID, ctx, t)
		if err != nil {
			log.Printf("while getting alerts: %v\n", err)
			return []SpecificAlert{}
		}
		return selectSpecificAlerts(alerts, languages)
	}
	return []SpecificAlert{}
}

func getVehiclePositions(ctx Context, t *Traffic, lb, rt Position) []VehicleStatus {
	feedInfo, err := getFeedInfo(ctx.DataHome, ctx.FeedID, ctx.Version)
	if err != nil {
		log.Printf("while getting feedInfo: %v\n", err)
		feedInfo = FeedInfo{}
	}

	var function func(Context, Position, Position) ([]VehicleStatus, error)
	if feedInfo.Name != "" {
		if _, ok := feedInfo.RealtimeFeeds[VEHICLE_POSITIONS]; ok {
			function = getGtfsRealtimeVehicles
		} else if isLuaVehiclesScript(ctx) {
			function = getLuaRealtimeVehicles
		}
	}
	if function != nil {
		statuses, err := function(ctx, lb, rt)
		if err != nil {
			log.Printf("while getting vehicle positions: %v\n", err)
			return []VehicleStatus{}
		}
		ids := make([]string, len(statuses))
		for i, status := range statuses {
			ids[i] = status.TripID
		}
		trips, err := GetTrips(ids, ctx, t)
		if err != nil {
			log.Printf("while getting trips: %v", err)
		}
		statusesWithLine := make([]VehicleStatus, len(statuses))
		for i, status := range statuses {
			if status.LineID != "" && status.Headsign != "" {
				statusesWithLine[i] = status
			} else {
				if _, ok := trips[status.TripID]; !ok {
					continue
				}
				status.LineID = trips[status.TripID].LineID
				status.Headsign = trips[status.TripID].Headsign
				statusesWithLine[i] = status
			}
		}
		return statusesWithLine
	}
	return []VehicleStatus{}
}
